Skip to content

fix: Always put the well-known endpoints at the server root #1288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

seanhoughton
Copy link

Bug Report: Incorrect .well-known/oauth-protected-resource endpoint path when resource_server_url ends with /sse

This is reported in 1264

Summary

When configuring FastMCP with OAuth2 authentication and setting the resource_server_url to end with /sse (as required by VSCode MCP clients), FastMCP incorrectly serves the .well-known/oauth-protected-resource endpoint at /sse/.well-known/oauth-protected-resource instead of the expected root path /.well-known/oauth-protected-resource.

Environment

  • FastMCP version: Part of mcp Python library (check with pip show mcp)
  • Python version: 3.13
  • Operating System: macOS
  • MCP Client: VSCode with MCP extension
  • Affected Files:
    • mcp/server/fastmcp/server.py (lines ~790-797)
    • mcp/server/auth/routes.py (lines ~215-224)

Expected Behavior

  1. The .well-known/oauth-protected-resource endpoint should always be served at the root path (/.well-known/oauth-protected-resource) regardless of the resource_server_url configuration
  2. The resource field in the .well-known response should point to the actual protected resource (e.g., /sse)
  3. OAuth2 discovery should work correctly with MCP clients like VSCode

Actual Behavior

When resource_server_url is set to http://localhost:8099/sse, FastMCP:

  1. Serves .well-known/oauth-protected-resource at /sse/.well-known/oauth-protected-resource
  2. The SSE endpoint /sse returns a www-authenticate header with resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"
  3. OAuth2 discovery fails because clients expect the .well-known endpoint at the root

Steps to Reproduce

  1. Create a FastMCP server with OAuth2 configuration:
from mcp.server.fastmcp import FastMCP
from mcp.server.auth.settings import AuthSettings
from pydantic import AnyHttpUrl

# Configure auth settings with /sse endpoint
auth_settings = AuthSettings(
    issuer_url=AnyHttpUrl("https://login.microsoftonline.com/tenant-id/v2.0"),
    resource_server_url=AnyHttpUrl("http://localhost:8099/sse"),  # Note: ends with /sse
    required_scopes=["https://example.com/scope"]
)

mcp = FastMCP(
    "Test Server",
    token_verifier=your_token_verifier,
    auth=auth_settings,
)

app = mcp.sse_app()
  1. Start the server: uvicorn server:app --port 8099

  2. Test the endpoints:

# This should work but returns 404
curl http://localhost:8099/.well-known/oauth-protected-resource

# This works but shouldn't be the location
curl http://localhost:8099/sse/.well-known/oauth-protected-resource

# SSE endpoint references wrong .well-known location
curl -I http://localhost:8099/sse
# Returns: resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"

Root Cause Analysis

Exact Location of Bug:

  • File: mcp/server/fastmcp/server.py
  • Lines: ~790-797 in the sse_app() method
  • Function: FastMCP.sse_app()

The Issue: When setting up OAuth2 authentication, FastMCP constructs the resource_metadata_url incorrectly:

# BUGGY CODE - Line ~790-797 in sse_app() method
resource_metadata_url = AnyHttpUrl(
    str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)

When resource_server_url is http://localhost:8099/sse, this creates http://localhost:8099/sse/.well-known/oauth-protected-resource.

However, the actual .well-known endpoint is created by create_protected_resource_routes() (in mcp/server/auth/routes.py lines ~215-224), which always creates it at the root path:

# CORRECT CODE - This always creates /.well-known/oauth-protected-resource at root
return [
    Route(
        "/.well-known/oauth-protected-resource",
        endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]),
        methods=["GET", "OPTIONS"],
    )
]

The Fix: The resource_metadata_url should be constructed from the base URL, not the resource_server_url:

# PROPOSED FIX
if self.settings.auth and self.settings.auth.resource_server_url:
    from pydantic import AnyHttpUrl
    from urllib.parse import urlparse
    
    # Extract base URL from resource_server_url
    parsed = urlparse(str(self.settings.auth.resource_server_url))
    base_url = f"{parsed.scheme}://{parsed.netloc}"
    
    resource_metadata_url = AnyHttpUrl(
        base_url + "/.well-known/oauth-protected-resource"
    )

Impact

  • High: Breaks OAuth2 discovery for MCP clients like VSCode
  • MCP servers cannot be properly authenticated when using the recommended /sse resource URL pattern
  • Workarounds require custom endpoint overrides, defeating the purpose of built-in auth support

Proposed Solution

The .well-known/oauth-protected-resource endpoint should always be served at the root path (/.well-known/oauth-protected-resource), regardless of the resource_server_url configuration. The resource_server_url should only affect:

  1. The resource field value in the .well-known response
  2. The resource_metadata reference in www-authenticate headers

Current Workaround

Override the built-in .well-known endpoint with a custom implementation:

async def custom_well_known_endpoint(request):
    return JSONResponse({
        "resource": f"{config.EXTERNAL_ADDRESS}/sse",
        "authorization_servers": ["https://login.microsoftonline.com/tenant/v2.0"],
        "scopes_supported": ["https://example.com/scope"],
        "bearer_methods_supported": ["header"]
    })

# Override the built-in endpoint
app.router.routes.insert(0, Route("/.well-known/oauth-protected-resource", custom_well_known_endpoint, methods=["GET"]))

Additional Context

  • This issue specifically affects integration with VSCode MCP clients, which require the resource URL to end with /sse
  • The OAuth2 specification (RFC 8414) defines .well-known endpoints should be at predictable root paths
  • Other OAuth2 implementations (e.g., Auth0, Okta) serve .well-known endpoints at root regardless of resource configuration

Related Documentation

# Bug Report: Incorrect `.well-known/oauth-protected-resource` endpoint path when `resource_server_url` ends with `/sse`

## Summary
When configuring FastMCP with OAuth2 authentication and setting the `resource_server_url` to end with `/sse` (as required by VSCode MCP clients), FastMCP incorrectly serves the `.well-known/oauth-protected-resource` endpoint at `/sse/.well-known/oauth-protected-resource` instead of the expected root path `/.well-known/oauth-protected-resource`.

## Environment
- **FastMCP version**: Part of `mcp` Python library (check with `pip show mcp`)
- **Python version**: 3.13
- **Operating System**: macOS
- **MCP Client**: VSCode with MCP extension
- **Affected Files**: 
  - `mcp/server/fastmcp/server.py` (lines ~790-797)
  - `mcp/server/auth/routes.py` (lines ~215-224)

## Expected Behavior
1. The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`) regardless of the `resource_server_url` configuration
2. The `resource` field in the `.well-known` response should point to the actual protected resource (e.g., `/sse`)
3. OAuth2 discovery should work correctly with MCP clients like VSCode

## Actual Behavior
When `resource_server_url` is set to `http://localhost:8099/sse`, FastMCP:
1. Serves `.well-known/oauth-protected-resource` at `/sse/.well-known/oauth-protected-resource`
2. The SSE endpoint `/sse` returns a `www-authenticate` header with `resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"`
3. OAuth2 discovery fails because clients expect the `.well-known` endpoint at the root

## Steps to Reproduce

1. Create a FastMCP server with OAuth2 configuration:

```python
from mcp.server.fastmcp import FastMCP
from mcp.server.auth.settings import AuthSettings
from pydantic import AnyHttpUrl

# Configure auth settings with /sse endpoint
auth_settings = AuthSettings(
    issuer_url=AnyHttpUrl("https://login.microsoftonline.com/tenant-id/v2.0"),
    resource_server_url=AnyHttpUrl("http://localhost:8099/sse"),  # Note: ends with /sse
    required_scopes=["https://example.com/scope"]
)

mcp = FastMCP(
    "Test Server",
    token_verifier=your_token_verifier,
    auth=auth_settings,
)

app = mcp.sse_app()
```

2. Start the server: `uvicorn server:app --port 8099`

3. Test the endpoints:
```bash
# This should work but returns 404
curl http://localhost:8099/.well-known/oauth-protected-resource

# This works but shouldn't be the location
curl http://localhost:8099/sse/.well-known/oauth-protected-resource

# SSE endpoint references wrong .well-known location
curl -I http://localhost:8099/sse
# Returns: resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"
```

## Root Cause Analysis

**Exact Location of Bug**: 
- **File**: `mcp/server/fastmcp/server.py` 
- **Lines**: ~790-797 in the `sse_app()` method
- **Function**: `FastMCP.sse_app()`

**The Issue**: When setting up OAuth2 authentication, FastMCP constructs the `resource_metadata_url` incorrectly:

```python
# BUGGY CODE - Line ~790-797 in sse_app() method
resource_metadata_url = AnyHttpUrl(
    str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)
```

When `resource_server_url` is `http://localhost:8099/sse`, this creates `http://localhost:8099/sse/.well-known/oauth-protected-resource`.

However, the actual `.well-known` endpoint is created by `create_protected_resource_routes()` (in `mcp/server/auth/routes.py` lines ~215-224), which always creates it at the root path:

```python
# CORRECT CODE - This always creates /.well-known/oauth-protected-resource at root
return [
    Route(
        "/.well-known/oauth-protected-resource",
        endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]),
        methods=["GET", "OPTIONS"],
    )
]
```

**The Fix**: The `resource_metadata_url` should be constructed from the base URL, not the `resource_server_url`:

```python
# PROPOSED FIX
if self.settings.auth and self.settings.auth.resource_server_url:
    from pydantic import AnyHttpUrl
    from urllib.parse import urlparse
    
    # Extract base URL from resource_server_url
    parsed = urlparse(str(self.settings.auth.resource_server_url))
    base_url = f"{parsed.scheme}://{parsed.netloc}"
    
    resource_metadata_url = AnyHttpUrl(
        base_url + "/.well-known/oauth-protected-resource"
    )
```

## Impact
- **High**: Breaks OAuth2 discovery for MCP clients like VSCode
- MCP servers cannot be properly authenticated when using the recommended `/sse` resource URL pattern
- Workarounds require custom endpoint overrides, defeating the purpose of built-in auth support

## Proposed Solution
The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`), regardless of the `resource_server_url` configuration. The `resource_server_url` should only affect:
1. The `resource` field value in the `.well-known` response
2. The `resource_metadata` reference in `www-authenticate` headers

## Current Workaround
Override the built-in `.well-known` endpoint with a custom implementation:

```python
async def custom_well_known_endpoint(request):
    return JSONResponse({
        "resource": f"{config.EXTERNAL_ADDRESS}/sse",
        "authorization_servers": ["https://login.microsoftonline.com/tenant/v2.0"],
        "scopes_supported": ["https://example.com/scope"],
        "bearer_methods_supported": ["header"]
    })

# Override the built-in endpoint
app.router.routes.insert(0, Route("/.well-known/oauth-protected-resource", custom_well_known_endpoint, methods=["GET"]))
```

## Additional Context
- This issue specifically affects integration with VSCode MCP clients, which require the resource URL to end with `/sse`
- The OAuth2 specification (RFC 8414) defines `.well-known` endpoints should be at predictable root paths
- Other OAuth2 implementations (e.g., Auth0, Okta) serve `.well-known` endpoints at root regardless of resource configuration

## Related Documentation
- [RFC 8414 - OAuth 2.0 Authorization Server Metadata](https://tools.ietf.org/html/rfc8414)
- [MCP OAuth2 Authentication Documentation](https://modelcontextprotocol.io/docs/concepts/authentication)
@seanhoughton seanhoughton requested a review from a team as a code owner August 20, 2025 14:50
@seanhoughton seanhoughton requested a review from ihrpr August 20, 2025 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant